/**
 * 参考 http://www.javaeye.com/topic/198525
 */
package com.ambow.core.configuration;
import java.io.IOException;
import java.util.HashSet;
import java.util.Set;

import javax.persistence.Entity;

import org.apache.log4j.Logger;
import org.apache.oro.text.regex.MalformedPatternException;
import org.apache.oro.text.regex.Pattern;
import org.apache.oro.text.regex.PatternCompiler;
import org.apache.oro.text.regex.PatternMatcher;
import org.apache.oro.text.regex.Perl5Compiler;
import org.apache.oro.text.regex.Perl5Matcher;
import org.hibernate.HibernateException;
import org.hibernate.cfg.AnnotationConfiguration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean;
import org.springframework.util.ClassUtils;
import org.springframework.util.CollectionUtils;

import com.ambow.core.exception.BusinessException;

public class AnnotationSessionFactoryBeanEx
		extends AnnotationSessionFactoryBean {
	  private static final Logger logger = Logger.getLogger(AnnotationSessionFactoryBeanEx.class);   
  
  /**  
   * The locations of the hibernate enity class files. They are often some of the string with  
   * Sping-style resource. A ".class" subfix can make the scaning more precise.  
   * <p> example:  
   * <pre>  
   * classpath*:com/systop/** /model/*.class  
   * </pre>  
   */  
  private String[] annotatedClassesLocations;   
  
  /**  
   * Which classes are not included in the session.  
   * They are some of the regular expression.  
   */  
  private String[] excludedClassesRegexPatterns;     
  
  /**  
   * @param annotatedClassesLocations the annotatedClassesLocations to set  
   */  
  public void setAnnotatedClassesLocations(String[] annotatedClassesLocations) {   
    this.annotatedClassesLocations = annotatedClassesLocations;   
  }   
  
  /**  
   * @see AnnotationSessionFactoryBeanEx#postProcessAnnotationConfiguration(org.hibernate.cfg.AnnotationConfiguration)  
   */  
  @Override  
  protected void postProcessAnnotationConfiguration(AnnotationConfiguration config)   
      throws HibernateException {   
    Set<Class> annClasses = scanAnnotatedClasses(); //Scan enity classes.   
    // Add entity classes to the configuration.   
    if (!CollectionUtils.isEmpty(annClasses)) {   
      for (Class annClass : annClasses) {   
        config.addAnnotatedClass(annClass);   
      }   
    }   
  }   
     
  /**  
   * Scan annotated hibernate classes in the locations.  
   * @return Set of the annotated classes, if no matched class, return empty Set.  
   */  
  private Set<Class> scanAnnotatedClasses() {   
    ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();   
    MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(   
        resourcePatternResolver);   
    Set<Class> annotatedClasses = new HashSet<Class>();   
    if (annotatedClassesLocations != null) {   
      try {   
        for (String annClassesLocation : annotatedClassesLocations) {   
          //Resolve the resources   
          Resource[] resources = resourcePatternResolver.getResources(annClassesLocation);   
          for (Resource resource : resources) {   
            MetadataReader metadataReader = metadataReaderFactory.getMetadataReader(resource);   
            String className = metadataReader.getClassMetadata().getClassName();   
            //If the class is hibernate enity class, and it does not match the excluded class patterns.   
            if (isEntityClass(metadataReader) && !isExcludedClass(className)) {   
              Class clazz = ClassUtils.forName(className);   
              annotatedClasses.add(clazz);   
              logger.debug("A entity class has been found."+clazz.getName());   
            }   
          }   
  
        }   
      } catch (IOException e) {   
        logger.error("I/O failure during classpath scanning, "+e.getMessage());   
        e.printStackTrace();   
        throw new BusinessException(e.getMessage());   
      } catch (ClassNotFoundException e) {   
        logger.error("Class not found, "+e.getMessage());   
        e.printStackTrace();   
        throw new BusinessException(e.getMessage());   
      } catch (LinkageError e) {   
        logger.error("LinkageError "+ e.getMessage());   
        e.printStackTrace();   
        throw new BusinessException(e.getMessage());   
      }   
    }   
  
    return annotatedClasses;   
  }   
     
  /**  
   * @return True if the given MetadataReader shows   
   * that the class is annotated by <code>javax.persistence.Enity</code>  
   */  
  private boolean isEntityClass(MetadataReader metadataReader) {   
    Set<String> annTypes = metadataReader.getAnnotationMetadata().getAnnotationTypes();   
    if (CollectionUtils.isEmpty(annTypes)) {   
      return false;   
    }   
       
    return annTypes.contains(Entity.class.getName());   
  }   
     
  /**  
   *   
   * @return True if the given class name match the excluded class patterns.   
   */  
  private boolean isExcludedClass(String className) {   
    if (excludedClassesRegexPatterns == null) { // All class is included.   
      return false;   
    }   
  
    PatternCompiler compiler = new Perl5Compiler();   
    PatternMatcher matcher = new Perl5Matcher();   
    try {   
      for (String regex : excludedClassesRegexPatterns) { //Test each patterns.   
        logger.debug("Pattern is: "+regex);   
        Pattern pattern = compiler.compile(regex);   
        if (matcher.matches(className, pattern)) {   
          logger.debug("class "+className+", matches [{}], so it is excluded."+pattern.getPattern());   
          return true;   
        }   
      }   
    } catch (MalformedPatternException e) {   
      logger.warn("Malformed pattern"+e.getMessage());   
    }   
  
    return false;   
  }   
  
  /**  
   * @param exculdePatterns the exculdePatterns to set  
   */  
  public void setExcludedClassesRegexPatterns(String[] excludedClassesRegexPatterns) {   
    this.excludedClassesRegexPatterns = excludedClassesRegexPatterns;   
  }   

}
